<?php

/**
 * Class QRData
 *
 * @created      25.11.2015
 *
 * @author       Smiley <smiley@chillerlan.net>
 * @copyright    2015 Smiley
 * @license      MIT
 */
declare(strict_types=1);

namespace zxf\QrCode\Data;

use zxf\QrCode\Common\BitBuffer;
use zxf\QrCode\Common\EccLevel;
use zxf\QrCode\Common\Mode;
use zxf\QrCode\Common\Version;
use zxf\QrCode\QROptions;
use zxf\QrCode\Settings\SettingsContainerInterface;

use function sprintf;

/**
 * Processes the binary data and maps it on a QRMatrix which is then being returned
 */
final class QRData
{
    /**
     * the options instance
     */
    private SettingsContainerInterface|QROptions $options;

    /**
     * a BitBuffer instance
     */
    private BitBuffer $bitBuffer;

    /**
     * an EccLevel instance
     */
    private EccLevel $eccLevel;

    /**
     * current QR Code version
     */
    private Version $version;

    /**
     * @var \zxf\QrCode\Data\QRDataModeInterface[]
     */
    private array $dataSegments = [];

    /**
     * Max bits for the current ECC mode
     *
     * @var int[]
     */
    private array $maxBitsForEcc;

    /**
     * QRData constructor.
     *
     * @param  \zxf\QrCode\Data\QRDataModeInterface[]  $dataSegments
     */
    public function __construct(SettingsContainerInterface|QROptions $options, array $dataSegments = [])
    {
        $this->options = $options;
        $this->bitBuffer = new BitBuffer;
        $this->eccLevel = new EccLevel($this->options->eccLevel);
        $this->maxBitsForEcc = $this->eccLevel->getMaxBits();

        $this->setData($dataSegments);
    }

    /**
     * Sets the data string (internally called by the constructor)
     *
     * Subsequent calls will overwrite the current state - use the QrCode::add*Segement() method instead
     *
     * @param  \zxf\QrCode\Data\QRDataModeInterface[]  $dataSegments
     */
    public function setData(array $dataSegments): self
    {
        $this->dataSegments = $dataSegments;
        $this->version = $this->getMinimumVersion();

        $this->bitBuffer->clear();
        $this->writeBitBuffer();

        return $this;
    }

    /**
     * Returns the current BitBuffer instance
     *
     * @codeCoverageIgnore
     */
    public function getBitBuffer(): BitBuffer
    {
        return $this->bitBuffer;
    }

    /**
     * Sets a BitBuffer object
     *
     * This can be used instead of setData(), however, the version auto-detection is not available in this case.
     * The version needs to match the length bits range for the data mode the data has been encoded with,
     * additionally the bit array needs to contain enough pad bits.
     *
     * @throws \zxf\QrCode\Data\QRCodeDataException
     */
    public function setBitBuffer(BitBuffer $bitBuffer): self
    {

        if ($this->options->version === Version::AUTO) {
            throw new QRCodeDataException('version auto detection is not available');
        }

        if ($bitBuffer->getLength() === 0) {
            throw new QRCodeDataException('the given BitBuffer is empty');
        }

        $this->dataSegments = [];
        $this->bitBuffer = $bitBuffer;
        $this->version = new Version($this->options->version);

        return $this;
    }

    /**
     * returns a fresh matrix object with the data written and masked with the given $maskPattern
     */
    public function writeMatrix(): QRMatrix
    {
        return (new QRMatrix($this->version, $this->eccLevel))
            ->initFunctionalPatterns()
            ->writeCodewords($this->bitBuffer);
    }

    /**
     * estimates the total length of the several mode segments in order to guess the minimum version
     *
     * @throws \zxf\QrCode\Data\QRCodeDataException
     */
    public function estimateTotalBitLength(): int
    {
        $length = 0;

        foreach ($this->dataSegments as $segment) {
            // data length of the current segment
            $length += $segment->getLengthInBits();
            // +4 bits for the mode descriptor
            $length += 4;
            // Hanzi mode sets an additional 4 bit long subset identifier
            if ($segment instanceof Hanzi) {
                $length += 4;
            }
        }

        $provisionalVersion = null;
        /** @var int $version */
        foreach ($this->maxBitsForEcc as $version => $maxBits) {

            if ($length <= $maxBits) {
                $provisionalVersion = $version;
            }

        }

        if ($provisionalVersion !== null) {

            // add character count indicator bits for the provisional version
            foreach ($this->dataSegments as $segment) {
                $length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
            }

            // it seems that in some cases the estimated total length is not 100% accurate,
            // so we substract 4 bits from the total when not in mixed mode
            if (count($this->dataSegments) <= 1) {
                $length -= 4;
            }

            // we've got a match!
            // or let's see if there's a higher version number available
            if ($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])) {
                return $length;
            }

        }

        throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
    }

    /**
     * returns the minimum version number for the given string
     *
     * @throws \zxf\QrCode\Data\QRCodeDataException
     */
    public function getMinimumVersion(): Version
    {

        if ($this->options->version !== Version::AUTO) {
            return new Version($this->options->version);
        }

        $total = $this->estimateTotalBitLength();

        // guess the version number within the given range
        for ($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++) {
            if ($total <= ($this->maxBitsForEcc[$version] - 4)) {
                return new Version($version);
            }
        }

        // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
        throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
    }

    /**
     * creates a BitBuffer and writes the string data to it
     *
     * @throws \zxf\QrCode\Data\QRCodeDataException on data overflow
     */
    private function writeBitBuffer(): void
    {
        $MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);

        foreach ($this->dataSegments as $segment) {
            $segment->write($this->bitBuffer, $this->version->getVersionNumber());
        }

        // overflow, likely caused due to invalid version setting
        if ($this->bitBuffer->getLength() > $MAX_BITS) {
            throw new QRCodeDataException(
                sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS),
            );
        }

        // add terminator (ISO/IEC 18004:2000 Table 2)
        if (($this->bitBuffer->getLength() + 4) <= $MAX_BITS) {
            $this->bitBuffer->put(Mode::TERMINATOR, 4);
        }

        // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion

        // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
        // by the addition of padding bits with binary value 0
        while (($this->bitBuffer->getLength() % 8) !== 0) {

            if ($this->bitBuffer->getLength() === $MAX_BITS) {
                break;
            }

            $this->bitBuffer->putBit(false);
        }

        // The message bit stream shall then be extended to fill the data capacity of the symbol
        // corresponding to the Version and Error Correction Level, by the addition of the Pad
        // Codewords 11101100 and 00010001 alternately.
        $alternate = false;

        while (($this->bitBuffer->getLength() + 8) <= $MAX_BITS) {
            $this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);

            $alternate = ! $alternate;
        }

        // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
        // to the end of the message in order exactly to fill the symbol capacity
        while ($this->bitBuffer->getLength() <= $MAX_BITS) {
            $this->bitBuffer->putBit(false);
        }

    }
}
